Skip to content

Update fee and dust handling for zero fee channels #3884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

carlaKC
Copy link
Contributor

@carlaKC carlaKC commented Jun 23, 2025

This PR completes the off-chain handling of V3 channels, as described in #3789.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 23, 2025

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@carlaKC
Copy link
Contributor Author

carlaKC commented Jun 23, 2025

This still needs a few tests, opening up early for conceptual review.

@@ -5078,6 +5092,7 @@ where
) -> u64 {
if funding.get_channel_type().supports_anchor_zero_fee_commitments() {
debug_assert_eq!(self.feerate_per_kw, 0);
debug_assert!(fee_spike_buffer_htlc.is_none());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether it's worth having this assertion (which makes it necessary to check channel type before calling fee functions). The alternative would be to just ignore the parameters completely for zero fee channels (even though Some fee_spike_buffer_htlc doesn't make sense for the type).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't say I have a strong preference. The assertion seems fine to me.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All LGTM, didn't carefully check every hunk, though.

@@ -133,6 +134,22 @@ enum FeeUpdateState {
Outbound,
}

/// Returns the fees for success and timeout second stage HTLC transactions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Probably belongs in chan_utils.rs given it kinda mirrors commit_and_htlc_tx_fees_sat and is also called from there?

@@ -5078,6 +5092,7 @@ where
) -> u64 {
if funding.get_channel_type().supports_anchor_zero_fee_commitments() {
debug_assert_eq!(self.feerate_per_kw, 0);
debug_assert!(fee_spike_buffer_htlc.is_none());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't say I have a strong preference. The assertion seems fine to me.

@carlaKC carlaKC force-pushed the 3789-fees-and-dust branch from 001c8e9 to 4157d24 Compare July 10, 2025 13:16
@carlaKC carlaKC marked this pull request as ready for review July 10, 2025 13:32
@carlaKC carlaKC removed the request for review from joostjager July 10, 2025 13:32
carlaKC and others added 10 commits July 10, 2025 09:39
This fee rate is currently used in two scenarios:
- To count any fees above what we consider to be a sane estimate towards
  our dust exposure.
- To get a maximum dust exposure (when using
  MaxDustHTLCExposure::FeeEstimator strategy).

When we have zero fee commitments:
- Commitments are zero fee, so we don't need to count fees towards dust
  exposure.
- The amount of dust we have is not dependent on fees, as everything is
  zero fee.
- We still want to limit our total dust exposure.

This commit updates get_dust_exposure_limiting_feerate to allow a None
value to prepare for support for zero fee commitments. This clearly
allows us to indicate when we don't care about fee rates for dust
considerations.

In get_max_dust_htlc_exposure_msat, we simply hardcode a value of
1 sat/vbyte if a feerate dependent strategy is being used.
LDK does not support a channel type that supports both zero fee and
nonzero fee anchors (as this is impossible), so we can drop the
unnecessary check in build_htlc_output.
This commit pulls calculation of second stage fees into a helper
function. A side effect of this refactor is that it fixes a rounding
issue in commit_and_htlc_tx_fees_sat. Previously, rounding down of
feerate would happen after multiplying by the number of HTLCs. Now the
feerate will be rounded down before multiplying by the number of HTLCs.

This wasn't a serious issue - it would just cause us very slightly
over estimate our dust exposure at certain feerates that needed
rounding. A hard-coded value in test_nondust_htlc_excess_fees_are_dust
is updated to account for this rounding change.
Update second_stage_tx_fees_sat to return zero for zero fee commitments.
As is the case for anchors_zero_fee_commitments, this changes ensures
that we won't trim the second stage transaction fee off of the HTLC
amount.
When we have zero fee commitments, we don't need to calculate our fee
rate or check that it isn't stale because it is always zero.

Co-authored-by: Matt Corallo <[email protected]>
Update test helper in preparation to test zero fee commitments, where
the local balance will differ from the previously hardcoded value.
@carlaKC carlaKC force-pushed the 3789-fees-and-dust branch from 4157d24 to 5fe6fa7 Compare July 10, 2025 13:40
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@@ -4775,7 +4779,8 @@ where
let excess_feerate_opt = outbound_feerate_update
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth debug asserting that this is always 0 for zero-fee commitments ?

#[rustfmt::skip]
pub(crate) fn htlc_tx_fees_sat(feerate_per_kw: u32, num_accepted_htlcs: usize, num_offered_htlcs: usize, channel_type_features: &ChannelTypeFeatures) -> u64 {
let htlc_tx_fees_sat = if !channel_type_features.supports_anchors_zero_fee_htlc_tx() {
num_accepted_htlcs as u64 * htlc_success_tx_weight(channel_type_features) * feerate_per_kw as u64 / 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the commit message, I wouldn't say we round a feerate, but rather round a raw transaction fee amount.

let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) =
second_stage_tx_fees_sat(features, feerate_per_kw);

let htlc_tx_fee_sat = if local {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: keep the formatting consistent between the two is_dust functions ?

Comment on lines +1408 to +1411
// Sender should not queue an update_fee message.
nodes[0].node.timer_tick_occurred();
let events_0 = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(events_0.len(), 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect an update_fee to be generated in any channel type here.

This test has this log here:

node 0 TRACE [lightning::ln::channelmanager:6975]       Channel 5109930e509248b4fdac5f8fef66915f693393cdbfd8f0cd2d56f0f4d80baf62 qualifies for a feerate change from 0 to 0.

Looks like we should update the condition to skip a fee update in fn update_channel_fee:

if new_feerate <= chan.context.get_feerate_sat_per_1000_weight() && new_feerate * 2 > chan.context.get_feerate_sat_per_1000_weight() {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we should update the condition to skip a fee update in fn update_channel_fee:

I don't really feel like it's channel manger's responsibility to know that we shouldn't be sending fee updates for zero fee commitments (belongs in channel IMO), but checking "has this fee changed" is fair game so will look at updating 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right seems the condition is true for all cases where the old feerate is equal to the new feerate, except when that feerate is 0 :)

// side, only on the sender's. Note that with anchor outputs we are no longer as
// sensitive to fee spikes, so we need to account for them.
// `Some(())` is for the fee spike buffer we keep for the remote if the channel is
// not zero fee.. This deviates from the spec because the fee spike buffer requirement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: delete the two dots

Copy link
Contributor

@elnosh elnosh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still grokking some of the changes here and the zero-fee commitment proposal but from my kind of limited context it LGTM (:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Goal: Open
Development

Successfully merging this pull request may close these issues.

5 participants